package com.example.logdemo.log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;

/**
 *
 *
 * @author zhiyuan
 * @date 2025/10/28
 */
@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    private static final String[] SENSITIVE_KEYS = new String[]{
            "password","secret","token","accessToken","refreshToken","authorization","auth","passwd","pwd","creditCard"
    };

    @Around("@annotation(com.example.logdemo.log.Loggable) || @within(com.example.logdemo.log.Loggable)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();

        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();

        Loggable ann = method.getAnnotation(Loggable.class);
        if (ann == null) {
            ann = pjp.getTarget().getClass().getAnnotation(Loggable.class);
        }

        String methodName = signature.getDeclaringType().getSimpleName() + "." + method.getName();
        String tag = (ann != null) ? ann.tag() : "";
        boolean logArgs = ann == null || ann.logArgs();
        boolean logResult = ann == null || ann.logResult();

        Map<String, Object> argMap = buildArgsMap(signature.getParameterNames(), pjp.getArgs(), logArgs);

        String traceId = MDC.get("traceId");
        log.info("➡️  Enter {} tag={} traceId={} args={}", methodName, tag, traceId, argMap);

        Object result = null;
        Throwable error = null;
        try {
            result = pjp.proceed();
            return result;
        } catch (Throwable t) {
            error = t;
            throw t;
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (error == null) {
                if (logResult) {
                    log.info("✅ Exit {} traceId={} cost={}ms result={}", methodName, traceId, cost, safeToString(result));
                } else {
                    log.info("✅ Exit {} traceId={} cost={}ms", methodName, traceId, cost);
                }
            } else {
                log.error("❌ Error {} traceId={} cost={}ms ex={} msg={}", methodName, traceId, cost,
                        error.getClass().getSimpleName(), error.getMessage(), error);
            }
        }
    }

    private Map<String, Object> buildArgsMap(String[] names, Object[] values, boolean logArgs) {
        Map<String, Object> map = new HashMap<>();
        if (!logArgs) return map;
        if (names == null || values == null) return map;

        IntStream.range(0, Math.min(names.length, values.length)).forEach(i -> {
            String name = names[i];
            Object value = values[i];
            if (value == null) {
                map.put(name, null);
                return;
            }
            // Skip heavy/unsafe types
            if (value instanceof org.springframework.web.multipart.MultipartFile) {
                map.put(name, "[MultipartFile]");
                return;
            }
            if (value instanceof jakarta.servlet.http.HttpServletRequest) {
                map.put(name, "[HttpServletRequest]");
                return;
            }
            if (value instanceof jakarta.servlet.http.HttpServletResponse) {
                map.put(name, "[HttpServletResponse]");
                return;
            }
            // Mask sensitive values by param name heuristic
            if (isSensitiveKey(name)) {
                map.put(name, "******");
                return;
            }
            // Mask common headers if arg is HttpHeaders
            if (value instanceof HttpHeaders headers) {
                HttpHeaders masked = new HttpHeaders();
                headers.forEach((k, v) -> masked.put(k, isSensitiveKey(k) ? Arrays.asList("******") : v));
                map.put(name, masked);
                return;
            }
            map.put(name, safeToString(value));
        });
        return map;
    }

    private boolean isSensitiveKey(String key) {
        String k = key == null ? "" : key.toLowerCase();
        for (String s : SENSITIVE_KEYS) {
            if (k.contains(s.toLowerCase())) return true;
        }
        return false;
    }

    private String safeToString(Object obj) {
        if (obj == null) return "null";
        try {
            return String.valueOf(obj);
        } catch (Throwable t) {
            return obj.getClass().getName() + "@(toString-error)";
        }
    }
}
